/*
 * Copyright 2024 the original author or authors.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.gradle.buildinit.specs.internal;

import org.gradle.api.logging.Logger;
import org.gradle.api.logging.Logging;
import org.gradle.buildinit.specs.BuildInitGenerator;
import org.gradle.buildinit.specs.BuildInitSpec;
import org.gradle.internal.service.scopes.Scope;
import org.gradle.internal.service.scopes.ServiceScope;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.stream.Collectors;

/**
 * A registry of the available {@link BuildInitSpec}s that can be used to generate new projects via the {@code init} task.
 */
@ServiceScope(Scope.Build.class)
public final class BuildInitSpecRegistry {
    public static final String BUILD_INIT_SPECS_PLUGIN_SUPPLIER = "org.gradle.buildinit.specs";

    private static final Logger LOGGER = Logging.getLogger(BuildInitSpecRegistry.class);

    private final Map<Class<? extends BuildInitGenerator>, List<BuildInitSpec>> specsByGeneratorType = new HashMap<>();

    /**
     * Register the given generator as providing the given.
     * <p>
     * This does not replace existing mappings for the same generator class, but appends to any that are already
     * present in the registry.  Attempting to register a spec for the same type with multiple generators will
     * produce an exception.
     *
     * @param generatorType generator class to use to generate specs
     * @param specs new {@link BuildInitSpec}s that can be generated by this generator
     */
    public void register(Class<? extends BuildInitGenerator> generatorType, List<BuildInitSpec> specs) {
        List<BuildInitSpec> currentSpecsForGenerator = specsByGeneratorType.computeIfAbsent(generatorType, k -> new ArrayList<>());
        specs.forEach(spec -> {
            doGetGeneratorForSpec(spec).ifPresent(g -> {
                throw new IllegalStateException(String.format("Spec: '%s' with type: '%s' cannot use same type as another spec already registered!", spec.getDisplayName(), spec.getType()));
            });
            currentSpecsForGenerator.add(spec);
            LOGGER.info("Loaded project spec: '{} ({})', generated via: '{}'", spec.getDisplayName(), spec.getType(), generatorType.getName());
        });
    }

    public List<BuildInitSpec> getAllSpecs() {
        return specsByGeneratorType.values().stream().flatMap(List::stream).collect(Collectors.toList());
    }

    public boolean isEmpty() {
        return specsByGeneratorType.isEmpty();
    }

    /**
     * Returns the {@link BuildInitSpec} in the registry with the given type; throwing
     * an exception if there is not exactly one such spec.
     *
     * @param type the type of the project spec to find
     * @return the project spec with the given type
     */
    public BuildInitSpec getSpecByType(String type) {
        List<BuildInitSpec> matchingSpecs = specsByGeneratorType.values().stream()
            .flatMap(List::stream)
            .filter(spec -> Objects.equals(spec.getType(), type))
            .collect(Collectors.toList());

        switch (matchingSpecs.size()) {
            case 0:
                throw new IllegalStateException("Build init spec with type: '" + type + "' was not found!" + System.lineSeparator() +
                    "Known types:" + System.lineSeparator() +
                    getAllSpecs().stream()
                        .map(BuildInitSpec::getType)
                        .map(t -> " - " + t)
                        .collect(Collectors.joining(System.lineSeparator()))
                );
            case 1:
                return matchingSpecs.get(0);
            default:
                throw new IllegalStateException("Multiple project specs: " + matchingSpecs.stream().map(BuildInitSpec::getDisplayName).collect(Collectors.joining(", ")) + " with type: '" + type + "' were found!");
        }
    }

    /**
     * Returns the {@link BuildInitGenerator} type that can be used to generate a project
     * with the given {@link BuildInitSpec}.
     * <p>
     * This searches by project type.
     *
     * @param spec the project spec to find the generator for
     * @return the type of generator that can be used to generate a project with the given spec
     */
    public Class<? extends BuildInitGenerator> getGeneratorForSpec(BuildInitSpec spec) {
        return doGetGeneratorForSpec(spec).orElseThrow(() -> new IllegalStateException("Spec: '" + spec.getDisplayName() + "' with type: '" + spec.getType() + "' is not registered!"));
    }

    private Optional<Class<? extends BuildInitGenerator>> doGetGeneratorForSpec(BuildInitSpec spec) {
        return specsByGeneratorType.entrySet().stream()
            .filter(entry -> isSpecWithTypePresent(spec, entry.getValue()))
            .findFirst()
            .map(Map.Entry::getKey);
    }

    private boolean isSpecWithTypePresent(BuildInitSpec target, List<BuildInitSpec> toSearch) {
        return toSearch.stream().anyMatch(s -> isSameType(s, target));
    }

    private boolean isSameType(BuildInitSpec s1, BuildInitSpec s2) {
        return Objects.equals(s1.getType(), s2.getType());
    }
}
